Repository Pattern
4 min readRepository Pattern
TL;DR
Mediate between the domain and the data-access layer — expose persistence as an in-memory-like collection behind an interface, so the domain never depends on EF Core, SQL, or the ORM.
How it works
🧩 Example — The domain owns the interface, infrastructure provides the implementation
// Domain / application layer — the contract lives WITH the domain (Dependency Inversion)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task<IReadOnlyList<Order>> GetPendingAsync();
Task AddAsync(Order order);
void Remove(Order order);
}
// Infrastructure layer — EF Core implementation, swappable for Dapper / Mongo / a fake
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public Task<Order?> GetByIdAsync(Guid id) =>
_db.Orders.FirstOrDefaultAsync(o => o.Id == id);
public async Task<IReadOnlyList<Order>> GetPendingAsync() =>
await _db.Orders.Where(o => o.Status == OrderStatus.Pending).ToListAsync();
public async Task AddAsync(Order order) => await _db.Orders.AddAsync(order);
public void Remove(Order order) => _db.Orders.Remove(order);
}
// --- Usage (application service) ---
public class CancelOrderHandler
{
private readonly IOrderRepository _orders;
private readonly IUnitOfWork _uow;
public CancelOrderHandler(IOrderRepository orders, IUnitOfWork uow)
{
_orders = orders;
_uow = uow;
}
public async Task Handle(Guid orderId)
{
var order = await _orders.GetByIdAsync(orderId)
?? throw new NotFoundException(orderId);
order.Cancel(); // domain behaviour
await _uow.SaveChangesAsync(); // Unit of Work commits the transaction
}
}
✅ Why it matters:
- The domain/application code depends on
IOrderRepository, not EF Core — you can swap Dapper, Mongo, or an in-memory fake without touching business logic. - Centralises query logic (
GetPendingAsync) so it isn't copy-pasted across controllers and services. - Makes the domain trivially unit-testable with an in-memory fake repository — no database, no mocking framework.
- Pairs with Unit of Work to commit many changes across aggregates in one transaction.
Quick recall Q&A
It decouples the domain from data-access concerns. The domain talks to a collection-like interface (Add, GetById, Remove) and stays ignorant of EF Core, SQL, or the storage engine — honouring the Dependency Inversion Principle.
In the domain/application layer, not infrastructure. The domain owns the contract; infrastructure provides the implementation. That inverts the dependency so the inner layers never reference the database.
Repository handles per-aggregate reads/writes; Unit of Work tracks changes across repositories and commits them in a single transaction. EF Core's DbContext already plays the Unit of Work role and SaveChanges is the commit.
Yes — DbSet<T> is a repository and DbContext is a Unit of Work. A thin generic repository over EF Core often adds little. Add your own only when you want to hide ORM types, centralise complex queries, or keep the domain persistence-ignorant.
IRepository<T> vs. specific repositories — which should you prefer?Specific repositories (IOrderRepository) express intent and expose only the queries the aggregate needs. A generic IRepository<T> cuts boilerplate but encourages CRUD-for-everything and leaky IQueryable methods. Prefer specific; use a generic base only for shared plumbing.
IQueryable<T> from a repository considered a leak?It lets callers compose queries the repository can't control or test, and leaks provider-specific translation, lazy evaluation, and N+1 risks across the boundary. Return materialised results (IReadOnlyList<T>) or accept a Specification object instead.
Use the Specification pattern — pass a query object describing the criteria — or expose intent-revealing methods per use case. Don't add a new method for every ad-hoc filter.
Application and domain code depend on the interface, so tests inject an in-memory fake (a List<T>-backed repository) and assert behaviour with no database and fast feedback.
Over-abstraction on top of an ORM that is already a repository/UoW, leaky abstractions (IQueryable escaping), and generic repositories that become a dumping ground. The fix is to apply it deliberately — for domain isolation and query encapsulation — not reflexively.
It's the boundary between the domain and the persistence adapter: one repository per aggregate root, interface in the domain, implementation in infrastructure, wired by DI — keeping the dependency arrows pointing inward.